const path = require('path')
const fs = require('fs')
const babylon = require('babylon')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
const { SyncHook } = require('tapable')

// babylon 主要是把源码 转换成AST
// @babel/traverse
// @babel/types
// @babel/generator

class Compiler {
  constructor(config) {
    // entry output
    this.config = config
    // 需要保存入口文件的路径
    this.entryId // './src/index.js'
    // 需要保存所有依赖的模块
    this.modules = {}

    this.entry = config.entry // 入口路径
    this.root = process.cwd() // 工作路径
    this.hooks = {
      entryOption: new SyncHook(),
      compiler: new SyncHook(),
      afterCompiler: new SyncHook(),
      afterPlugin: new SyncHook(),
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook()
    }

    // 如果传递了plugins参数
    const plugins = this.config.plugins
    if (Array.isArray(plugins)) {
      plugins.forEach(plugin => {
        plugin.apply(this)
      })
    }
    this.hooks.afterPlugin.call()
  }

  getSource(modulePath) {
    const rules = this.config.module.rules || []
    let content = fs.readFileSync(modulePath, 'utf-8')
    // 拿到每个规则loader来处理
    for (let i = 0; i < rules.length; i++) {
      const rule = rules[i];
      const { test, use } = rule
      let len = use.length - 1
      if (test.test(modulePath)) { // 这个模块需要通过loader来进行换行
        // 获取对应的loader 函数
        function normalLoader() {
          const loader = require(use[len--])
          content = loader(content)
          // 递归调用loader 实现转换功能
          if (len >= 0) {
            normalLoader()
          }
        }
        normalLoader()
      }
      
    }
    return content
  }

  parse(source, parentPath) { // AST 解析语法树
    const ast = babylon.parse(source)
    const dependencies = [] // 依赖数组
    traverse(ast, {
      CallExpression(p) { //  a() require()
        const node = p.node
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'
          let moduleName = node.arguments[0].value // 取到的就是模块引用名字
          moduleName += (path.extname(moduleName) ? '' : '.js') // ./a.js
          moduleName = './' + path.join(parentPath, moduleName) // src/a.sj
          node.arguments  = [t.stringLiteral(moduleName)]
          dependencies.push(moduleName)
        }
      }
    })

    const sourceCode = generator(ast).code
    
    return { sourceCode, dependencies }
  }

  // 构建模块
  buildModule(modulePath, isEntry) {
    // 拿到模块内容
    const source = this.getSource(modulePath)

    // 拿到id modulePath = modulePath - this.root
    const moduleName = './' + path.relative(this.root, modulePath)

    if (isEntry) {
      this.entryId = moduleName // 保存入口的名字
    }

    // 解析需要把source源码进行改造 返回一个依赖列表
    const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) // ./src
    // console.log(sourceCode, dependencies);
    // 把相对路径和模块中的内容 对应起来
    this.modules[moduleName] = sourceCode

    dependencies.forEach(dep => { // 副模块的加载 递归加载
      this.buildModule(path.join(this.root, dep), false)
    })
  }

  // 发射文件
  emitFile() {
    // 用数据渲染
    // 拿到输出到哪个目录下
    // 输出路径
    const main = path.join(this.config.output.path, this.config.output.filename)
    // 模板路径
    this.templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
    const code = ejs.render(this.templateStr, { entryId: this.entryId, modules: this.modules })

    this.assets = {}
    this.assets[main] = code
    fs.writeFileSync(main, code)
  }

  run() {
    this.hooks.run.call()
    // 执行 并且创建模块的依赖关系
    this.hooks.compiler.call()
    this.buildModule(path.resolve(this.root, this.entry), true)
    this.hooks.afterCompiler.call()
    // console.log(this.modules, this.entryId);
    // 发射一个文件 打包后的文件
    this.emitFile()
    this.hooks.emit.call()
    this.hooks.done.call()
  }
}

module.exports = Compiler